/*
 * Copyright (c) 2014 Hai Bison
 *
 * See the file LICENSE at the root directory of this project for copying
 * permission.
 */

package haibison.simpleprovider;

import static android.provider.BaseColumns._ID;
import static haibison.simpleprovider.BuildConfig.DEBUG;
import static haibison.simpleprovider.SimpleContract._DATE_CREATED;
import static haibison.simpleprovider.SimpleContract._DATE_MODIFIED;
import haibison.simpleprovider.utils.Ids;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;

/**
 * Simple provider. This provider makes use of {@link SimpleContract}.
 * <p/>
 * <h1>Usage</h1>
 * <p/>
 * <ul>
 * <li>In {@link #onCreate()}: Register your contracts with one of
 * {@code registerContract()} methods.</li>
 * <li>Implement all other abstract methods.</li>
 * </ul>
 *
 * @author Hai Bison
 *
 */
public abstract class SimpleProvider extends ContentProvider {

    private static final String CLASSNAME = SimpleProvider.class.getName();

    /**
     * Library name.
     */
    public static final String LIB_NAME = "Simple Provider";

    /**
     * Library version name.
     */
    public static final String LIB_VERSION_NAME = "1.0.1 beta";

    /**
     * Library version code.
     */
    public static final int LIB_VERSION_CODE = 4;

    /**
     * Unique ID for this library. It's used for things such as database file
     * names...
     */
    public static final String UID = "8f0c7dd3-8671-464d-982e-fa8d93658938";

    /**
     * Invalid URI.
     */
    protected static final int INVALID_URI = -1;

    /**
     * Contract.
     *
     * @author Hai Bison
     *
     */
    public static class Contract {

        /**
         * List of URI IDs content.
         */
        private static final List<Integer> URI_IDS_CONTENT = new ArrayList<Integer>();

        /**
         * List of URI IDs content item.
         */
        private static final List<Integer> URI_IDS_CONTENT_ITEM = new ArrayList<Integer>();

        /**
         * Checks to see if given ID is content URI ID.
         *
         * @param id
         *            the ID.
         * @return {@code true} or {@code false}.
         */
        public static final boolean isContentUriId(int id) {
            return URI_IDS_CONTENT.contains(id);
        }// isContentUriId()

        /**
         * Checks to see if given ID is content item URI ID.
         *
         * @param id
         *            the ID.
         * @return {@code true} or {@code false}.
         */
        public static final boolean isContentItemUriId(int id) {
            return URI_IDS_CONTENT_ITEM.contains(id);
        }// isContentItemUriId()

        /**
         * Table name.
         */
        public final String table;

        /**
         * Default sort order, maybe {@code null}.
         */
        public final String defaultSortOrder;

        /**
         * The URI ID content.
         */
        private final int uriIdContent = Ids.newUid();

        /**
         * The URI ID content item.
         */
        private final int uriIdContentItem = Ids.newUid();

        /**
         * Creates new instance.
         *
         * @param table
         *            see {@link #table}.
         * @param defaultSortOrder
         *            see {@link #defaultSortOrder}.
         */
        public Contract(String table, String defaultSortOrder) {
            this.table = table;
            this.defaultSortOrder = defaultSortOrder;

            URI_IDS_CONTENT.add(uriIdContent);
            URI_IDS_CONTENT_ITEM.add(uriIdContentItem);
        }// Contract()

    }// Contract()

    /**
     * This is map of authorities to their corresponding contracts.
     */
    private static final Map<String, Contract> MAP_CONTRACTS = new HashMap<String, Contract>();

    /**
     * The URI matcher.
     */
    private static final UriMatcher URI_MATCHER = new UriMatcher(
            UriMatcher.NO_MATCH);

    /**
     * Registers contract.
     *
     * @param authority
     *            the authority.
     * @param table
     *            table name.
     * @param defaultSortOrder
     *            default sort order, can be {@code null}.
     */
    protected static void registerContract(String authority, String table,
            String defaultSortOrder) {
        registerContract(authority, new Contract(table, defaultSortOrder));
    }// registerContract()

    /**
     * Registers contract.
     *
     * @param authority
     *            the authority.
     * @param contract
     *            the contract.
     */
    protected static void registerContract(String authority, Contract contract) {
        MAP_CONTRACTS.put(authority, contract);

        URI_MATCHER.addURI(authority, null, contract.uriIdContent);
        URI_MATCHER.addURI(authority, "#", contract.uriIdContentItem);
    }// registerContract()

    /**
     * The DB helper.
     */
    private SQLiteOpenHelper mDbHelper;

    /**
     * Creates new instance of DB helper. Later you should use
     * {@link #getDbHelper()} to access this instance.
     *
     * @return new instance of DB helper.
     */
    protected abstract SQLiteOpenHelper newDbHelper();

    /**
     * Gets the instance of DB helper (which is created by
     * {@link #newDbHelper()}).
     *
     * @return the instance of DB helper.
     */
    protected SQLiteOpenHelper getDbHelper() {
        if (mDbHelper == null)
            mDbHelper = newDbHelper();

        return mDbHelper;
    }// getDbHelper()

    /**
     * Restarts this provider.
     */
    public synchronized void restart() {
        if (mDbHelper == null)
            return;

        mDbHelper.close();
        mDbHelper = null;
    }// restart()

    /**
     * Gets table name of given URI.
     *
     * @param uri
     *            the URI.
     * @return table name, maybe {@code null} if not available.
     */
    protected String getTableName(Uri uri) {
        Contract contract = MAP_CONTRACTS.get(uri.getAuthority());
        return contract == null ? null : contract.table;
    }// getTableName()

    /**
     * Gets default sort order of given URI.
     *
     * @param uri
     *            the URI.
     * @return default sort order, maybe {@code null}.
     */
    protected String getDefaultSortOrder(Uri uri) {
        Contract contract = MAP_CONTRACTS.get(uri.getAuthority());
        return contract == null ? null : contract.defaultSortOrder;
    }// getDefaultSortOrder()

    /**
     * Gets URI type.
     *
     * @param uri
     *            the URI.
     * @return URI type, can be {@link #INVALID_URI}.
     */
    protected int getUriType(Uri uri) {
        return URI_MATCHER.match(uri);
    }// getUriType()

    /**
     * Checks to see if given URI is content URI.
     *
     * @param uri
     *            the URI.
     * @return {@code true} or {@code false}.
     */
    protected final boolean isContentUri(Uri uri) {
        return isContentUriId(getUriType(uri));
    }// isContentUri()

    /**
     * Checks to see if given ID is content URI ID.
     *
     * @param id
     *            the ID.
     * @return {@code true} or {@code false}.
     */
    protected final boolean isContentUriId(int id) {
        return Contract.isContentUriId(id);
    }// isContentUriId()

    /**
     * Checks to see if given URI is content item URI.
     *
     * @param uri
     *            the URI.
     * @return {@code true} or {@code false}.
     */
    protected final boolean isContentItemUri(Uri uri) {
        return isContentItemUriId(getUriType(uri));
    }// isContentItemUri()

    /**
     * Checks to see if given ID is content item URI ID.
     *
     * @param id
     *            the ID.
     * @return {@code true} or {@code false}.
     */
    protected final boolean isContentItemUriId(int id) {
        return Contract.isContentItemUriId(id);
    }// isContentItemUriId()

    @Override
    public boolean onCreate() {
        return true;
    }// onCreate()

    @Override
    public String getType(Uri uri) {
        final int uriType = URI_MATCHER.match(uri);

        if (uriType == INVALID_URI)
            return null;

        if (Contract.isContentUriId(uriType))
            return SimpleContract.getContentType(uri.getAuthority());
        if (Contract.isContentItemUriId(uriType))
            return SimpleContract.getContentItemType(uri.getAuthority());

        return null;
    }// getType()

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        if (DEBUG)
            Log.d(CLASSNAME, "delete() >> " + uri + " -- MATCHER="
                    + URI_MATCHER.match(uri));

        final int uriType = URI_MATCHER.match(uri);
        if (uriType == INVALID_URI)
            return -1;

        SQLiteDatabase db = getDbHelper().getWritableDatabase();
        String finalWhere;
        int count = 0;

        if (Contract.isContentUriId(uriType)) {
            count = db.delete(getTableName(uri), selection, selectionArgs);
        } else {
            finalWhere = _ID + "=" + uri.getLastPathSegment();
            if (selection != null)
                finalWhere = finalWhere + " AND " + selection;
            count = db.delete(getTableName(uri), finalWhere, selectionArgs);
        }

        if (count > 0 || selection == null)
            getContext().getContentResolver().notifyChange(uri, null);

        return count;
    }// delete()

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        if (DEBUG)
            Log.d(CLASSNAME, "insert() >> " + uri);

        final int uriType = URI_MATCHER.match(uri);
        if (uriType == INVALID_URI || !Contract.isContentUriId(uriType))
            return null;

        /*
         * Gets the current time in milliseconds
         */
        final long now = System.currentTimeMillis();
        /*
         * If the values map doesn't contain the creation date/modification
         * date, sets the value to current time.
         */
        for (String col : new String[] { _DATE_CREATED, _DATE_MODIFIED })
            if (!values.containsKey(col))
                values.put(col, SimpleContract.formatNumber(now, false));

        /*
         * Opens the database object in "write" mode.
         */
        SQLiteDatabase db = getDbHelper().getWritableDatabase();

        /*
         * Performs the insert and returns the ID of the new note.
         */
        long rowId = db.insert(getTableName(uri), null, values);

        /*
         * If the insert succeeded, the row ID exists.
         */
        if (rowId >= 0) {
            /*
             * Creates a URI with the note ID pattern and the new row ID
             * appended to it.
             */
            Uri noteUri = ContentUris.withAppendedId(
                    SimpleContract.getContentItemBaseUri(uri.getAuthority()),
                    rowId);

            /*
             * Notifies observers registered against this provider that the data
             * changed.
             */
            getContext().getContentResolver().notifyChange(noteUri, null);
            return noteUri;
        }// if

        return null;
    }// insert()

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        if (DEBUG)
            Log.d(CLASSNAME, "query() >> " + uri);

        final int uriType = URI_MATCHER.match(uri);
        if (uriType == INVALID_URI)
            return null;

        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        qb.setTables(getTableName(uri));

        SQLiteDatabase db = null;
        Cursor cursor = null;

        /*
         * Choose the projection and adjust the "where" clause based on URI
         * pattern-matching.
         */
        if (Contract.isContentUriId(uriType)) {
            /*
             * Do nothing.
             */
        } else {
            qb.appendWhere(_ID + "=" + uri.getLastPathSegment());
        }

        if (TextUtils.isEmpty(sortOrder))
            sortOrder = getDefaultSortOrder(uri);

        if (db == null) {
            db = getDbHelper().getReadableDatabase();
            cursor = qb.query(db, projection, selection, selectionArgs, null,
                    null, sortOrder);
        }

        if (cursor != null)
            cursor.setNotificationUri(getContext().getContentResolver(), uri);

        return cursor;
    }// query()

    @Override
    public int update(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        if (DEBUG)
            Log.d(CLASSNAME, "update() >> " + uri);

        final int uriType = URI_MATCHER.match(uri);
        if (uriType == INVALID_URI)
            return -1;

        SQLiteDatabase db = getDbHelper().getWritableDatabase();
        int count = 0;
        String finalWhere;

        if (!values.containsKey(_DATE_MODIFIED))
            values.put(_DATE_MODIFIED, SimpleContract.formatNumber(
                    System.currentTimeMillis(), false));

        if (Contract.isContentUriId(uriType)) {
            count = db.update(getTableName(uri), values, selection,
                    selectionArgs);
        } else {
            finalWhere = _ID + "=" + uri.getLastPathSegment();

            if (selection != null)
                finalWhere = finalWhere + " AND " + selection;

            count = db.update(getTableName(uri), values, finalWhere,
                    selectionArgs);
        }

        if (count > 0)
            getContext().getContentResolver().notifyChange(uri, null);

        return count;
    }// update()

}
